#
# Copyright (c) 2012-2014, Technical University of Denmark (DTU)
# All rights reserved.
# 
# The Flexhouse 2.0 platform ("this software") is licensed under the
# BSD 3-clause license.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#   - Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   - Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   - Neither the name of DTU nor the names of its contributors may be used to
#     endorse or promote products derived from this software without specific
#     prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

# Add Py4J library to path
import os, sys, inspect, signal
MODULE_DIR = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))
# Get project root dir
ROOT_DIR = MODULE_DIR
while (True):
    ROOT_DIR = os.path.dirname(ROOT_DIR)
    if ("src" not in ROOT_DIR):
        break

# Get necessary directories and jars
LIB_DIR = os.path.join(ROOT_DIR, "lib")
if LIB_DIR not in sys.path:
    sys.path.insert(0, LIB_DIR)

from py4j.java_gateway import JavaGateway, GatewayClient, java_import
from py4j.protocol import Py4JJavaError, Py4JNetworkError, Py4JError
import time
import threading
import atexit
import abc
import glob
from subprocess import Popen, PIPE
from Queue import *

class PythonPluginController(object):
    __metaclass__ = abc.ABCMeta
    
    def sensorUpdatedInternal(self, sensor):
        self.queue.put(lambda: self.sensorUpdated(sensor))
        self.event.set()

    def actuatorUpdatedInternal(self, actuator):
	self.queue.put(lambda: self.actuatorUpdated(actuator))
        self.event.set()

    def initialStateInternal(self, actuators):
	self.javaProxy = self.gateway.entry_point.mPythonController
	self.queue.put(lambda: self.initialStateReceived(actuators))
        self.event.set()
	
    def controllerParameterUpdatedInternal(self, controllerParameter, value):
	self.queue.put(lambda: self.controllerParameterUpdated(controllerParameter, value))
        self.event.set()
	
    def onVisualizationEventInternal(self, visualizationEvent):
	self.queue.put(lambda: self.onVisualizationEvent(visualizationEvent))
        self.event.set()
	
    def getControllerConfigInternal(self, controllerConfig):
	self.supplyControllerConfig(controllerConfig)

    ####################################################
    # These are the methods that the end user can/must implement
    @abc.abstractmethod
    def sensorUpdated(self, sensor):
        return

    def actuatorUpdated(self, actuator):
	return;

    def initialStateReceived(self, actuators):
	return;

    def controllerParameterUpdated(self, controllerParameter, value):
	return;

    def onVisualizationEvent(self, visualizationEvent):
	return;

    @abc.abstractmethod
    def supplyControllerConfig(self, controllerConfig):
	return None;

    ####################################################

    def doWork(self):
        while (self.isRunning):
            while (self.queue.empty() == False):
                # Poll queue and call the appropriate function
                f = self.queue.get()
                f()

            # Check if the process is shutting down - otherwise block
            if (self.isRunning):
                self.event.wait()
                self.event.clear()

    def doPrinting(self):
	while (self.isRunning):
	    print(self.process.stdout.readline().replace("\n",""))
    
    def init(self):
        # Create a work flag
        self.event = threading.Event()

        # Init eventqueue
        self.queue = Queue()

        # Start workerthread
        self.isRunning = True
        self.workerThread = threading.Thread(target=self.doWork)
        self.workerThread.start()

	# Print stdout
	self.printThread = threading.Thread(target=self.doPrinting)
	self.printThread.start()


    def shutdown(self, frame, signum):
	print("Shutting down Python controller...")
        self.isRunning = False
        self.event.set()
        self.workerThread.join()
	self.printThread.join()
	self.gateway.shutdown()
	self.process.terminate()

    def __init__(self, name, owner, cfgfile):
	# Get the platform type
	isWin = sys.platform.startswith('win');
    
	# Get dir of python file
	MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
	if (isWin):
	    MODULE_DIR = os.path.dirname(inspect.getsourcefile(lambda _: None))

        # Find the correct escape character based on OS
        escChar = ':'
        if (isWin):
            escChar = ';'

        # Get project root dir
        ROOT_DIR = MODULE_DIR
        while (True):
            ROOT_DIR = os.path.dirname(ROOT_DIR)
            if ("src" not in ROOT_DIR):
                break

        # Get necessary directories and jars
        LIB_DIR = os.path.join(ROOT_DIR, "lib")
        BIN_DIR = os.path.join(ROOT_DIR, "bin")
        JARS = escChar.join(glob.glob(LIB_DIR + '/*.jar'))

        # Get SYSLAB bin directory
        SYSLAB_BIN = os.path.join(os.path.dirname(ROOT_DIR), "syslab/bin")

        # Create classpath
        CLASSPATH = BIN_DIR + escChar + LIB_DIR + escChar + JARS + escChar + SYSLAB_BIN

        # Get the name of the main java class for interfacing with python
        NAME_PATH = os.path.relpath(MODULE_DIR, os.path.join(ROOT_DIR, "src"))
        if (isWin):
    	    classname = NAME_PATH.replace("\\", ".")+".PythonPluginController"
        else:
            classname = NAME_PATH.replace("/", ".")+".PythonPluginController"

	# Launch the gateway
	try:
	    # Make sure java executes from the project directory
	    os.chdir(ROOT_DIR)
	    gateway, self.process = launch_py4j_server(name, owner, cfgfile, classname, CLASSPATH, isWin)
	    
	    # Add imports
	    java_import(gateway.jvm, "risoe.syslab.model.CompositeMeasurement")
	    java_import(gateway.jvm, "risoe.syslab.fhnew.common.SystemConstants")

	    self.name = name
	    self.init()
	
	    # Attach to gateway
	    self.gateway = gateway
            self.gateway.entry_point.setPythonProxy(self)	

	except Py4JJavaError as e:
	    # This might happen if another PluginController by the same name is
	    # already in use, or if one of the DeviceTypes that this controller
	    # wants to take control of is already possesed by another controller.
	    java_exception_instance = e.java_exception
	    print str(java_exception_instance)
	    self.shutdown()
	    sys.exit()

	except Py4JNetworkError:
	    sys.exit("Error Connecting to Java Gateway")

	signal.signal(signal.SIGINT, self.shutdown)
	self.process.wait()

    def toString(self):
        return self.name

    class Java:
        implements = ['risoe.syslab.fhnew.controller.PythonPluginController$PythonProxy']


# TODO: Allow config argument
def launch_py4j_server(name, owner, cfgfile, classname, classpath, isWin):
    """
    Launch a py4j server process on an ephemeral port.  Returns a Py4J gateway
    connected to the server.  The server is configured to shut down when the
    Python process exits.
    """

    # Launch the server on an ephemeral port in a subprocess.
    if (isWin):
	pid = Popen(["java", "-classpath", classpath, classname, name, owner, cfgfile],
        	stdout=PIPE, stdin=PIPE, shell=True)
    else:
	pid = Popen(["java", "-classpath", classpath, classname, name, owner, cfgfile],
		stdout=PIPE, stdin=PIPE);

    # Determine which ephemeral port the server started on.
    port = int(pid.stdout.readline())

    # Determine which ephemeral port the python proxy server should be started on
    proxyport = int(pid.stdout.readline())

    # Setup the gateway.
    gateway = JavaGateway(GatewayClient(port=port), python_proxy_port = proxyport, start_callback_server=True, auto_field=True)
    return gateway, pid


if __name__ == '__main__':
    # This class should never be run directly. Notify user of error
    print("This class is the superclass of all Python Plugin Controllers. You must subclass it and start the child controller instead.")

